package com.gitee.swsk33.lucenedemo;

import com.gitee.swsk33.lucenedemo.model.MarkdownFile;
import com.gitee.swsk33.lucenedemo.model.SearchInput;
import com.gitee.swsk33.lucenedemo.model.SearchResult;
import com.gitee.swsk33.lucenedemo.util.AnalyzerUtils;
import com.gitee.swsk33.lucenedemo.util.LuceneQueryUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

/**
 * Lucene本地索引操作封装类<br>
 * 任何写入/更新/删除操作后，应当调用commit方法持久化到硬盘
 */
public class LuceneIndexOperator implements AutoCloseable {

	/**
	 * 索引目录
	 */
	private final Directory indexFolder;

	/**
	 * 分词器
	 */
	private final Analyzer analyzer;

	/**
	 * 索引读取器
	 */
	private volatile IndexReader reader;

	/**
	 * 搜索器对象
	 */
	private volatile IndexSearcher searcher;

	/**
	 * 索引写入器
	 */
	private final IndexWriter writer;

	/**
	 * 构造函数，使用默认中文分词器
	 *
	 * @param indexPath 索引目录位置
	 */
	public LuceneIndexOperator(String indexPath) throws Exception {
		// 初始化目录
		this.indexFolder = FSDirectory.open(Paths.get(indexPath));
		// 创建默认分词器
		this.analyzer = AnalyzerUtils.createDefaultAnalyzer();
		// 初始化读取写入器
		// 创建写入器
		this.writer = new IndexWriter(indexFolder, new IndexWriterConfig(analyzer));
		// 先执行一次写入操作，否则可能导致读取器没有必要文件而抛出异常
		this.writer.commit();
		// 创建读取器
		this.reader = DirectoryReader.open(indexFolder);
		// 创建搜索器
		this.searcher = new IndexSearcher(reader);
	}

	/**
	 * 添加文档到索引，若索引中存在id相同的文章，则会将其覆盖
	 *
	 * @param file Markdown文件对象
	 */
	public void addOrSetDocument(MarkdownFile file) throws Exception {
		// Document表示一个支持检索的文档对象
		// 其中可包含多个字段，每个字段对应内容，而内容可以支持检索
		Document document = new Document();
		// 添加字段到文档（如标题和内容）
		// StringField表示不分词字段
		// TextField表示分词字段
		// Field.Store.YES表示储存该字段内容，否则它只会被索引，而不会出现在搜索结果中
		document.add(new StringField("id", file.getId(), Field.Store.YES));
		document.add(new TextField("title", file.getTitle(), Field.Store.YES));
		document.add(new TextField("content", file.getContent(), Field.Store.YES));
		// 基于id更新文档，id不存在则会添加文档，存在则会覆盖对应id的文档索引
		writer.updateDocument(new Term("id", file.getId()), document);
	}

	/**
	 * 从索引删除文档
	 *
	 * @param id 删除的文档id
	 */
	public void deleteDocument(String id) throws Exception {
		writer.deleteDocuments(new Term("id", id));
	}

	/**
	 * 从索引删除全部文档
	 */
	public void deleteAllDocuments() throws Exception {
		writer.deleteAll();
	}

	/**
	 * 根据一个id获取文档索引
	 *
	 * @param id 文档id
	 * @return 文档对象，不存在返回null
	 */
	public Document getDocument(String id) throws Exception {
		// 根据id字段查找
		TopDocs results = searcher.search(new TermQuery(new Term("id", id)), 1);
		if (results.totalHits.value() == 0) {
			return null;
		}
		return reader.storedFields().document(results.scoreDocs[0].doc);
	}

	/**
	 * 列出索引中全部文档对象
	 *
	 * @return 全部文档对象
	 */
	public List<Document> listDocument() throws Exception {
		List<Document> docs = new ArrayList<>();
		// 遍历全部索引中文档
		int count = reader.numDocs();
		StoredFields storedFields = reader.storedFields();
		for (int i = 0; i < count; i++) {
			docs.add(storedFields.document(i));
		}
		return docs;
	}

	/**
	 * 全文查找内容
	 *
	 * @param input 查询条件
	 * @return 返回查找结果，无匹配项则返回空的列表
	 */
	public List<SearchResult> searchDocument(SearchInput input) throws Exception {
		List<SearchResult> results = new ArrayList<>();
		// 构建查询条件
		Query query = LuceneQueryUtils.buildQuery(input.getField(), input.getText(), analyzer);
		// 执行查询
		// 未设定查询跳过条件则直接查询
		TopDocs topDocs;
		if (input.getOffset() <= 0) {
			topDocs = searcher.search(query, input.getSize());
		} else {
			// 查询到跳过部分
			topDocs = searcher.search(query, input.getOffset());
			// 跳过部分的最后一个文档
			ScoreDoc lastSkip = topDocs.scoreDocs[topDocs.scoreDocs.length - 1];
			topDocs = searcher.searchAfter(lastSkip, query, input.getSize());
		}
		StoredFields storedFields = reader.storedFields();
		for (ScoreDoc hit : topDocs.scoreDocs) {
			Document document = storedFields.document(hit.doc);
			SearchResult result = new SearchResult(document.get("id"), document.get("title"), LuceneQueryUtils.getMatchHiLight(query, analyzer, document, input.getField()));
			results.add(result);
		}
		return results;
	}

	/**
	 * 仅返回匹配条件的文档数量
	 *
	 * @param input 查询条件
	 * @return 匹配查询条件的文档记录数量
	 */
	public long searchDocumentCount(SearchInput input) throws Exception {
		Query query = LuceneQueryUtils.buildQuery(input.getField(), input.getText(), analyzer);
		return searcher.count(query);
	}

	/**
	 * 刷新读取器，当本地索引被写入或者发生变化时，该方法应当被调用
	 */
	private synchronized void refresh() throws Exception {
		DirectoryReader newReader = DirectoryReader.openIfChanged((DirectoryReader) this.reader);
		if (newReader != null) {
			System.out.println("本地索引改变！");
			this.reader.close();
			this.reader = newReader;
			this.searcher = new IndexSearcher(reader);
		}
	}

	/**
	 * 提交更改，将会刷新读取器
	 */
	public void commit() throws Exception {
		writer.commit();
		refresh();
	}

	/**
	 * 关闭和释放资源
	 */
	@Override
	public void close() throws Exception {
		this.reader.close();
		this.writer.close();
		this.indexFolder.close();
	}

}